﻿Imports System.Globalization
Imports System.Reflection
Imports System.Data
Imports System.Data.Objects
Imports System.Data.Objects.DataClasses
Imports System.Collections
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Windows.Forms
Imports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Diagnostics
Imports System.Linq
Imports System.Linq.Expressions
Imports System.Text

''' <summary>
''' Exposes an ObjectSet in the current ObjectContext as a bindable data source.
''' </summary>
''' <remarks>
''' This class implements the IListSource interface and returns an IBindingList 
''' built on top of the underlying ObjectSet.
''' </remarks>
Public Class EntitySet
    Implements IListSource
    Implements IQueryable

#Region "Fields"

    Private m_EntityDataSource As EntityDataSource
    'EntityDataSource that created this set
    Private m_Query As IQueryable
    'The value of the property
    Private m_List As IEntityBindingList
    'Default view for this set
    Private m_PropertyInfo As PropertyInfo
    'The property on the object context that gets the objects in this set
    Private m_ElementType As Type
    'The type of object in this set
    Private m_LookupDictionary As ListDictionary
    'Lookup dictionary (used to show and edit related entities in grid cells)

#End Region

#Region "Constructors"

    ''' <summary>
    ''' Initializes a new instance of a <see cref="EntitySet"/>.
    ''' </summary>
    ''' <param name="ds"><see cref="EntityDataSource"/> that owns the entities.</param>
    ''' <param name="pi"><see cref="PropertyInfo"/> used to retrieve the set from the context.</param>
    Friend Sub New(ByVal ds As EntityDataSource, ByVal pi As PropertyInfo)
        Dim type = pi.PropertyType
        Debug.Assert(type.IsGenericType AndAlso type.GetGenericTypeDefinition() Is GetType(ObjectSet(Of )) AndAlso type.GetGenericArguments().Length = 1)

        m_EntityDataSource = ds
        m_PropertyInfo = pi
        m_ElementType = type.GetGenericArguments()(0)
    End Sub

#End Region

#Region "Object model"

    ''' <summary>
    ''' Gets the <see cref="EntityDataSource"/> that owns this entity set.
    ''' </summary>
    Public ReadOnly Property EntityDataSource() As EntityDataSource
        Get
            Return m_EntityDataSource
        End Get
    End Property

    ''' <summary>
    ''' Gets the name of this entity set.
    ''' </summary>
    Public ReadOnly Property Name() As String
        Get
            Return If(m_PropertyInfo IsNot Nothing, m_PropertyInfo.Name, Nothing)
        End Get
    End Property

    ''' <summary>
    ''' Gets the type of entity in this entity set.
    ''' </summary>
    ''' <remarks>
    ''' Name chosen for consistency with EntitySet.ElementType 
    ''' (EntityType would seem more appropriate).
    ''' </remarks>
    Public ReadOnly Property ElementType() As Type Implements IQueryable.ElementType
        Get
            Return m_ElementType
        End Get
    End Property

    ''' <summary>
    ''' Gets the IQueryable object that retrieves the entities in this set.
    ''' </summary>
    Public ReadOnly Property Query() As IQueryable
        Get
            If m_Query Is Nothing AndAlso m_EntityDataSource.ObjectContext IsNot Nothing AndAlso m_PropertyInfo IsNot Nothing Then
                m_Query = TryCast(m_PropertyInfo.GetValue(m_EntityDataSource.ObjectContext, Nothing), IQueryable)
            End If
            Return m_Query
        End Get
    End Property

    ''' <summary>
    ''' Gets a list of the entities in the set that have not been deleted or detached.
    ''' </summary>
    Public ReadOnly Property ActiveEntities() As IEnumerable
        Get
            Return GetActiveEntities(Me.Query)
        End Get
    End Property

    ''' <summary>
    ''' Gets a list of the entities in the set that have not been deleted or detached.
    ''' </summary>
    Friend Shared Function GetActiveEntities(ByVal query As IEnumerable) As IEnumerable

        Dim list As New List(Of Object)
        If query IsNot Nothing Then
            For Each item As Object In query
                If TypeOf item Is EntityObject Then
                    Dim state = DirectCast(item, EntityObject).EntityState
                    If Not state = EntityState.Deleted And Not state = EntityState.Detached Then
                        list.Add(item)
                    End If
                End If
            Next
        End If
        Return list
    End Function

    ''' <summary>
    ''' Cancels any pending changes on this entity set.
    ''' </summary>
    Friend Sub CancelChanges()
        If m_List IsNot Nothing AndAlso Me.Query IsNot Nothing Then
            m_EntityDataSource.ObjectContext.Refresh(RefreshMode.StoreWins, Me.Query)
            m_List.Refresh()
        End If
    End Sub

    ''' <summary>
    ''' Refreshes this set's view by re-loading from the database.
    ''' </summary>
    Public Sub RefreshView()
        If m_List IsNot Nothing AndAlso Me.Query IsNot Nothing Then
            m_EntityDataSource.ObjectContext.Refresh(RefreshMode.ClientWins, Me.Query)
            m_List.Refresh()
        End If
    End Sub

    ''' <summary>
    ''' Gets an <see cref="IBindingListView"/> that can be used as a data source for bound controls.
    ''' </summary>
    Public ReadOnly Property List() As IBindingListView
        Get
            Return GetBindingList()
        End Get
    End Property

    ''' <summary>
    ''' Gets a dictionary containing entities as keys and their string representation as values.
    ''' </summary>
    ''' <remarks>
    ''' The data map is useful for displaying and editing entities in grid cells.
    ''' </remarks>
    Public ReadOnly Property LookupDictionary() As ListDictionary
        Get
            If m_LookupDictionary Is Nothing Then
                m_LookupDictionary = BuildLookupDictionary()
            End If
            Return m_LookupDictionary
        End Get
    End Property
#End Region

#Region "IListSource"

    Private ReadOnly Property ContainsListCollection() As Boolean Implements IListSource.ContainsListCollection
        Get
            Return False
        End Get
    End Property

    Private Function GetList() As IList Implements IListSource.GetList
        Return GetBindingList()
    End Function

#End Region

#Region "IBindingListView Implementation"

    'Gets an IBindingListView for this entity set
    Private Function GetBindingList() As IBindingListView
        If m_List Is Nothing Then

            ' create list
            Dim listType = GetType(EntityBindingList(Of ))
            listType = listType.MakeGenericType(Me.ElementType)
            m_List = DirectCast(Activator.CreateInstance(listType, m_EntityDataSource, Me.Query, Me.Name), IEntityBindingList)

            ' listen to changes
            AddHandler m_List.ListChanged, AddressOf m_List_ListChanged
        End If
        Return m_List
    End Function

    'Update data map when list changes
    Private Sub m_List_ListChanged(ByVal sender As Object, e As ListChangedEventArgs)
        If m_LookupDictionary IsNot Nothing Then
            ' clear old dictionary
            m_LookupDictionary.Clear()

            ' build new dictionary
            Dim map = BuildLookupDictionary(m_List)
            For Each kvp In map
                m_LookupDictionary.Add(kvp.Key, kvp.Value)
            Next
        End If
    End Sub

    'Build a data map for this entity set
    Private Function BuildLookupDictionary() As ListDictionary
        Return BuildLookupDictionary(ActiveEntities)
    End Function

    Private Function BuildLookupDictionary(ByVal entities As IEnumerable) As ListDictionary
        'If the entity implements "ToString", then use it
        Dim mi = m_ElementType.GetMethod("ToString")
        If mi IsNot Nothing AndAlso mi.DeclaringType Is m_ElementType Then
            Dim list = New List(Of KVPair)()
            For Each item As Object In entities
                list.Add(New KVPair(item, item.ToString()))
            Next
        End If

        'Use "DefaultProperty"
        Dim atts = m_ElementType.GetCustomAttributes(GetType(DefaultPropertyAttribute), False)
        If atts IsNot Nothing AndAlso atts.Length > 0 Then
            Dim dpa = TryCast(atts(0), DefaultPropertyAttribute)
            Dim pi = m_ElementType.GetProperty(dpa.Name)
            If pi IsNot Nothing AndAlso pi.PropertyType Is GetType(String) Then
                Dim list = New List(Of KVPair)()
                For Each item As Object In entities
                    list.Add(New KVPair(item, DirectCast(pi.GetValue(item, Nothing), String)))
                Next
                Return BuildLookupDictionary(list)
            End If
        End If

        'No default property: look for properties of type string with 
        'Names that contain "Name" or "Description"
        For Each pi As PropertyInfo In m_ElementType.GetProperties()
            If pi.PropertyType Is GetType(String) Then
                If pi.Name.IndexOf("Name", StringComparison.OrdinalIgnoreCase) > -1 OrElse pi.Name.IndexOf("Description", StringComparison.OrdinalIgnoreCase) > -1 Then
                    Dim list = New List(Of KVPair)()
                    For Each item As Object In entities
                        list.Add(New KVPair(item, DirectCast(pi.GetValue(item, Nothing), String)))
                    Next
                    Return BuildLookupDictionary(list)
                End If
            End If
        Next

        'No dice
        Return Nothing
    End Function

    Private Function BuildLookupDictionary(ByVal list As List(Of KVPair)) As ListDictionary
        'Sort list display value
        list.Sort()

        'Create data map
        Dim map = New ListDictionary()
        For Each kvp As KVPair In list
            map.Add(kvp.Key, kvp.Value)
        Next

        Return map
    End Function

    Private Class KVPair
        Implements IComparable

        Private m_Key As Object
        Private m_Value As String

        Public Property Key() As Object
            Get
                Return m_Key
            End Get
            Set(ByVal value As Object)
                m_Key = value
            End Set
        End Property

        Public Property Value() As String
            Get
                Return m_Value
            End Get
            Set(ByVal value As String)
                m_Value = value
            End Set
        End Property

        Public Sub New(ByVal key As Object, ByVal value As String)
            Me.Key = key
            Me.Value = value
        End Sub

        Private Function CompareTo(ByVal obj As Object) As Integer Implements IComparable.CompareTo
            Return String.Compare(Me.Value, DirectCast(obj, KVPair).Value, StringComparison.OrdinalIgnoreCase)
        End Function
    End Class

#End Region

#Region "IQueryable"

    Private ReadOnly Property Expression() As Expression Implements IQueryable.Expression
        Get
            Return Me.Query.Expression
        End Get
    End Property

    Private ReadOnly Property Provider() As IQueryProvider Implements IQueryable.Provider
        Get
            Return Me.Query.Provider
        End Get
    End Property

    Private Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
        Return Me.Query.GetEnumerator()
    End Function

#End Region

End Class

''' <summary>
''' Collection of EntitySet objects.
''' </summary>
Public Class EntitySetCollection
    Inherits ObservableCollection(Of EntitySet)

    Default Public Overloads ReadOnly Property Item(ByVal name As String) As EntitySet
        Get
            Dim index = Me.IndexOf(name)
            Return If(index > -1, Me(CStr(index)), Nothing)
        End Get
    End Property

    Public Overloads Function Contains(ByVal name As String) As Boolean
        Return IndexOf(name) > -1
    End Function

    Public Overloads Function IndexOf(ByVal name As String) As Integer
        For i As Integer = 0 To Count - 1
            If Me(i).Name = name Then
                Return i
            End If
        Next
        Return -1
    End Function
End Class

''' <summary>
''' Dictionary that implements IListSource (used for implementing lookup dictionaries)
''' </summary>
''' 
Public Class ListDictionary
    Inherits Dictionary(Of Object, String)
    Implements IListSource

    Public ReadOnly Property ContainsListCollection() As Boolean Implements IListSource.ContainsListCollection
        Get
            Throw New NotImplementedException()
        End Get
    End Property

    Public Function GetList() As IList Implements IListSource.GetList
        Dim list = New List(Of KeyValuePair(Of Object, String))()

        For Each kvp In Me
            list.Add(kvp)
        Next

        Return list
    End Function
End Class
